import json
import re
import os

try:
    import requests
except ImportError:
    import subprocess
    import sys
    print("未检测到requests库，正在使用清华镜像自动安装...")
    subprocess.check_call([
        sys.executable, "-m", "pip", "install", "requests", "-i", "https://pypi.tuna.tsinghua.edu.cn/simple"
    ])
    import requests

import time
import shutil

# ==============================================================================
# ---                           配置区域                         ---
# ==============================================================================
# miao-plugin文件夹的绝对或相对路径
MIAO_PLUGIN_PATH = "miao-plugin"

# --- 以下路径将自动拼接，请勿修改 ---
MAPPING_FILE_PATH = os.path.join(MIAO_PLUGIN_PATH, "resources", "meta-sr", "material", "data.json")
MATERIAL_IMG_DIR = os.path.dirname(MAPPING_FILE_PATH)
CHARACTER_DIR_ABS_PATH = os.path.join(MIAO_PLUGIN_PATH, "resources", "meta-sr", "character")
# ------------------------------------

API_CHARACTER_LIST_URL = "https://api.hakush.in/hsr/new.json" # 别动
API_CHARACTER_DATA_URL = "https://api.hakush.in/hsr/data/cn/character/{}.json" # 别动
API_BASE_URL = "https://api.hakush.in/hsr/data/cn/item/{}.json" # 别动

USE_API_CHARACTER_LIST = True # 是否使用API获取角色列表
CUSTOM_CHARACTER_IDS = [] # 如果API获取角色列表没有你需要的角色，请在此处填写角色ID 示例：[1409,1410]
USE_ENHANCED_DATA = False # 如果角色是加强角色，是否使用加强后的数据(这里只判断技能/星魂/行迹)
AUTO_ADD_MISSING_MATERIALS = True # 当miao-plugin中不存在新材料时，是否自动将材料数据合并到miao-plugin中
# ==============================================================================
# ---                          脚本核心代码 （屎山3号）                     ---
# ==============================================================================
HIGHLIGHT_KEYWORDS = ['火', '冰', '风', '雷', '量子', '虚数'] # 物理 这货走在哪都不像是属性的样子

material_id_cache = {}
material_name_to_id_map = {}
material_mapping_data = {}
newly_added_material_ids = set()

def format_number(n):
    if isinstance(n, float):
        rounded_n = round(n, 5)
        if rounded_n == int(rounded_n):
            return int(rounded_n)
        return rounded_n
    return n

def recursively_format_numbers(obj):
    if isinstance(obj, dict):
        return {k: recursively_format_numbers(v) for k, v in obj.items()}
    elif isinstance(obj, list):
        return [recursively_format_numbers(v) for v in obj]
    elif isinstance(obj, float):
        return format_number(obj)
    return obj

def highlight_keywords(text, keywords):
    if not text or not keywords:
        return text
    pattern = re.compile(
        f"({'|'.join(keywords)})"
        r"(?![^<]*>)"
        # 确保不会乱加标签
        r"(?=\/|属性)",
        re.UNICODE
    )
    return pattern.sub(r'<span>\1</span>', text)

def clean_and_format_description(desc, highlight=True):
    if not desc:
        return ""

    cleaned_desc = re.sub(r'</?color(?:=#[0-9a-fA-F]+)?>', '', desc)
    cleaned_desc = cleaned_desc.replace('\\n', '<br />').replace('\n', '')
    cleaned_desc = cleaned_desc.replace('<unbreak>', '<nobr>').replace('</unbreak>', '</nobr>')
    if highlight:
        final_desc = highlight_keywords(cleaned_desc, HIGHLIGHT_KEYWORDS)
    else:
        final_desc = cleaned_desc
        
    return final_desc.strip()

def remove_chinese_quotes(text):
    text = re.sub(r'<i>「(.*?)」</i>', r'\1', text)
    text = text.replace('<i>', '').replace('</i>', '')
    text = text.replace('\\n', ' ').replace('\n', ' ')
    return text

def format_skill_description_and_tables(desc, tables):
    tables_to_remove = []
    for key, table_data in list(tables.items()):
        is_static = table_data.get("isSame", False) or len(table_data.get("values", [])) == 1
        if not is_static:
            continue

        static_value = table_data["values"][0] if table_data.get("values") else 0
        pattern = re.compile(rf'#({key})\[(i|f1)\](%)?')
        found = False
        def inline_repl(m):
            nonlocal found
            found = True
            has_percent = m.group(3) is not None
            value_str = f"{format_number(static_value * 100):g}%" if has_percent else f"{format_number(static_value):g}"
            return value_str
        desc, n = pattern.subn(inline_repl, desc)
        if found:
            tables_to_remove.append(key)

    for key in tables_to_remove:
        if key in tables:
            del tables[key]

    remaining_placeholders = re.findall(r'#(\d+)', desc)
    unique_ordered_old_keys = list(dict.fromkeys(
        p for p in remaining_placeholders if p in tables
    ))

    key_map = {old_key: str(i + 1) for i, old_key in enumerate(unique_ordered_old_keys)}

    final_tables = {key_map[old_key]: tables[old_key] for old_key in unique_ordered_old_keys if old_key in tables}

    def remap_replacer(match):
        old_key = match.group(1)
        if old_key in key_map:
            return f"${key_map[old_key]}{match.group(2)}"
        return match.group(0)
    
    final_desc = re.sub(r'#(\d+)(\[.*?\]%?)', remap_replacer, desc)

    return final_desc, final_tables

def add_new_material_to_mapping(item_id, item_data):
    if not item_data:
        return

    name = item_data.get("ItemName", f"未知材料_{item_id}")
    desc = item_data.get("ItemBGDesc", "")
    rarity = parse_rarity(item_data.get("Rarity"))
    comefrom_data = item_data.get("ItemComefrom", [])
    comefrom_list = [item['Desc'] for item in comefrom_data]
    comefrom_str = " ".join(comefrom_list)

    category = "material"
    mat_type = 701

    if "凝滞虚影" in comefrom_str:
        category, mat_type = "char", 201
    elif "历战余响" in comefrom_str:
        category, mat_type = "normal", 401
    else:
        if len(comefrom_data) < 5:
            mat_type = 301
        else:
            mat_type = 701
        category = "material"

    new_material_entry = {
        "id": item_id,
        "type": mat_type,
        "name": name,
        "desc": clean_and_format_description(desc, highlight=False),
        "star": rarity,
        "source": comefrom_list
    }

    material_mapping_data.setdefault(category, {})
    material_mapping_data[category][str(item_id)] = new_material_entry
    newly_added_material_ids.add(str(item_id))
    
    material_name_to_id_map[name] = item_id

    print(f"信息：发现新材料 '{name}' (ID: {item_id})，已分类为 [{category}, type: {mat_type}]，将在结束后写入 data.json。")

    download_material_image(item_id, name, category, MATERIAL_IMG_DIR)

def get_mapped_material_id(original_id):
    if not original_id or not isinstance(original_id, int) or original_id == 0:
        return "0"
    original_id_str = str(original_id)
    if original_id_str in material_id_cache:
        return material_id_cache[original_id_str]
    
    item_data = None
    try:
        url = API_BASE_URL.format(original_id)
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        item_data = response.json()
        # 针对110507特殊处理
        if original_id == 110507:
            item_name = "阳雷的遥想"
        else:
            item_name = item_data.get("ItemName")
        if not item_name:
            print(f"警告：ID {original_id} 的API返回缺少 'ItemName' 字段。")
            material_id_cache[original_id_str] = original_id_str
            return original_id_str
    except requests.exceptions.RequestException as e:
        print(f"错误：获取ID {original_id} 的API失败：{e}，已使用原始ID作为回退。")
        material_id_cache[original_id_str] = original_id_str
        return original_id_str
    
    time.sleep(0.05)
    
    mapped_id = material_name_to_id_map.get(item_name)
    if mapped_id:
        mapped_id_str = str(mapped_id)
        material_id_cache[original_id_str] = mapped_id_str
        return mapped_id_str
    else:
        print(f"警告：未找到物品 '{item_name}'（原始ID {original_id}）的映射，将使用原始ID。")
        if AUTO_ADD_MISSING_MATERIALS and original_id_str not in newly_added_material_ids:
            # 暂存新材料
            add_new_material_to_mapping(original_id, item_data)
        
        material_id_cache[original_id_str] = original_id_str
        return original_id_str

def parse_rarity(rarity_str):
    if not rarity_str:
        return 0

    rarity_map = {
        "Normal": 1,
        "NotNormal": 2,
        "Rare": 3,
        "VeryRare": 4,
        "SuperRare": 5
    }
    if rarity_str in rarity_map:
        return rarity_map[rarity_str]

    match = re.search(r'\d+', rarity_str)
    if match:
        return int(match.group(0))

    print(f"警告：无法解析未知的稀有度值 '{rarity_str}'，已记为0星。")
    return 0

def map_element_type(damage_type):
    element_map = {"Fire": "火", "Ice": "冰", "Wind": "风", "Thunder": "雷", "Quantum": "量子", "Imaginary": "虚数", "Physical": "物理"}
    return element_map.get(damage_type, "未知元素")

def map_base_type_to_weapon(base_type):
    type_map = {"Warrior": "毁灭", "Rogue": "巡猎", "Mage": "智识", "Shaman": "同谐", "Warlock": "虚无", "Knight": "存护", "Priest": "丰饶", "Memory": "记忆"}
    return type_map.get(base_type, "未知命途")

def map_skill_type(skill_type):
    type_map = {"Normal": "普攻", "BPSkill": "战技", "Ultra": "终结技", "Passive": "天赋", "Maze": "秘技", "MazeNormal": "普攻", None: "天赋"}
    return type_map.get(skill_type, "未知类型")

def map_skill_tag(skill_tag):
    tag_map = {"SingleAttack": "单攻", "Support": "辅助", "AoEAttack": "群攻", "Impair": "妨害", "Blast": "扩散", "MazeAttack": "", "Summon": "召唤", "Enhance": "强化", "Bounce": "弹射"}
    return tag_map.get(skill_tag, "未知标签")

def format_cons_description(desc, param_list=None):
    if not desc: return ""
    
    formatted_desc = clean_and_format_description(desc)

    if not param_list:
        return re.sub(r'#(\d+)\[(i|f1)\](%)?', '', formatted_desc).strip()

    def replacer(match):
        param_index = int(match.group(1)) - 1
        format_type = match.group(2)
        has_percent = match.group(3) is not None

        if not (0 <= param_index < len(param_list)): return ""

        param_value = param_list[param_index]

        if has_percent:
            value_str = f"{format_number(param_value * 100):g}%"
        else:
            value_str = f"{format_number(param_value):g}"
        return value_str

    pattern = re.compile(r'#(\d+)\[(i|f1)\](%)?')
    formatted_desc = pattern.sub(replacer, formatted_desc)

    return formatted_desc.strip()

def map_tree_buff_key(property_type):
    buff_key_map = {
        "SpeedDelta": "speed",
        "BreakDamageAddedRatioBase": "stance",
        "HPAddedRatio": "hpPct",
        "AttackAddedRatio": "atkPct",
        "DefenceAddedRatio": "defPct",
        "CriticalChanceBase": "cpct",
        "CriticalDamageBase": "cdmg",
        "StatusProbabilityBase": "effPct",
        "StatusResistanceBase": "effDef"
    }
    return buff_key_map.get(property_type, property_type)

def map_tree_buff_name(property_type):
    buff_name_map = {"SpeedDelta": "速度强化", "BreakDamageAddedRatioBase": "击破强化", "HPAddedRatio": "生命强化", "AttackAddedRatio": "攻击强化", "DefenceAddedRatio": "防御强化", "CriticalChanceBase": "暴击强化", "CriticalDamageBase": "暴伤强化", "StatusProbabilityBase": "效果命中强化", "StatusResistanceBase": "效果抵抗强化"}
    return buff_name_map.get(property_type, "属性强化")

def scale_tree_buff_value(property_type, value):
    if value is None: return None
    if property_type == "SpeedDelta": return int(value * 100)
    elif property_type == "BreakDamageAddedRatioBase": return round(value * 100, 1)
    elif property_type.endswith("Ratio") or property_type.endswith("Base"): return round(value * 100, 2)
    return value

def map_tree_buff_key_elem(property_type, name=None):
    element_map = {"Fire": "火", "Ice": "冰", "Wind": "风", "Elec": "雷", "Auantum": "量子", "Imaginary": "虚数", "Phy": "物理"}

    if name:
        m = re.search(r'[•·](\w+)', name)
        if m:
            cn = m.group(1)
            for k, v in element_map.items():
                if v == cn:
                    return k.lower()
    return None

def convert_character_data(source_data, filename=""):
    if USE_ENHANCED_DATA:
        enhanced_wrapper = source_data.get("Enhanced")
        if enhanced_wrapper and isinstance(enhanced_wrapper, dict) and enhanced_wrapper:
            enhanced_data = next(iter(enhanced_wrapper.values()), None)
            if enhanced_data and isinstance(enhanced_data, dict):
                print(f"检测到并使用加强数据源将进行替换初始 技能、星魂、行迹...")
                if "Ranks" in enhanced_data: source_data["Ranks"] = enhanced_data.get("Ranks", {})
                if "Skills" in enhanced_data: source_data["Skills"] = enhanced_data.get("Skills", {})
                if "SkillTrees" in enhanced_data: source_data["SkillTrees"] = enhanced_data.get("SkillTrees", {})

    target_data = {}
    character_id = int(os.path.splitext(filename)[0]) if filename and os.path.splitext(filename)[0].isdigit() else 0

    live_camp, live_cncv, live_jpcv = None, None, None
    try:
        live_url = f"https://api.hakush.in/hsr/live/cn/character/{character_id}.json"
        resp = requests.get(live_url, timeout=10)
        if resp.status_code == 200:
            live_data = resp.json()
            chara_info = live_data.get("CharaInfo", {})
            live_camp = chara_info.get("Camp")
            va = chara_info.get("VA", {})
            live_cncv = va.get("Chinese")
            live_jpcv = va.get("Japanese")
    except Exception: pass

    target_data.update({
        "id": character_id,
        "key": source_data.get("AvatarVOTag", "unknown") or "unknown",
        "name": source_data.get("Name", "？？？") or "？？？",
        "star": parse_rarity(source_data.get("Rarity")),
        "elem": map_element_type(source_data.get("DamageType")),
        "allegiance": live_camp or source_data.get("CharaInfo", {}).get("Camp", "？？？") or "？？？",
        "weapon": map_base_type_to_weapon(source_data.get("BaseType")),
        "sp": source_data.get("SPNeed", 0) or 0,
        "desc": remove_chinese_quotes(source_data.get("Desc")),
        "cncv": live_cncv or source_data.get("CharaInfo", {}).get("VA", {}).get("Chinese", "？？？") or "？？？",
        "jpcv": live_jpcv or source_data.get("CharaInfo", {}).get("VA", {}).get("Japanese", "？？？") or "？？？"
    })

    stats_data_all = source_data.get("Stats", {})
    max_promo_key = max(stats_data_all.keys(), key=lambda x: int(x) if x.isdigit() else -1, default='0')
    max_promo_stats = stats_data_all.get(max_promo_key, {})
    level_0_stats = stats_data_all.get("0", {})
    grow_atk = level_0_stats.get("AttackAdd", 0.0) or 0.0
    grow_hp = level_0_stats.get("HPAdd", 0.0) or 0.0
    grow_def = level_0_stats.get("DefenceAdd", 0.0) or 0.0
    base_atk_80 = max_promo_stats.get("AttackBase", 0.0) or 0.0
    base_hp_80 = max_promo_stats.get("HPBase", 0.0) or 0.0
    base_def_80 = max_promo_stats.get("DefenceBase", 0.0) or 0.0

    target_data["baseAttr"] = {
        "atk": grow_atk * 79 + base_atk_80, "hp": grow_hp * 79 + base_hp_80, "def": grow_def * 79 + base_def_80,
        "speed": max_promo_stats.get("SpeedBase", 0) or 0,
        "cpct": round((max_promo_stats.get("CriticalChance", 0.0) or 0.0) * 100, 2),
        "cdmg": round((max_promo_stats.get("CriticalDamage", 0.0) or 0.0) * 100, 2),
        "aggro": max_promo_stats.get("BaseAggro", 0) or 0
    }
    target_data["growAttr"] = {"atk": format_number(grow_atk), "hp": format_number(grow_hp), "def": format_number(grow_def), "speed": 0}
    
    target_data.update({"talentId": {}, "talentCons": {}, "talent": {}})
    extra_skills_to_merge = {}

    def smart_table_name(desc, key, table, used_names):
        patterns = {
            lambda m: f"{m.group(1)}提高": r'(攻击力|生命上限|防御力|速度|暴击率|暴击伤害|击破特攻|受到的伤害|造成的伤害|伤害|伤害倍率|抗性穿透)提高<nobr>\$'+str(key)+r'\[(i|f1)\](%)?</nobr>(\+)?',
            "伤害额外提高": r'造成的伤害.*?额外提高<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>',
            "持续伤害": r'受到.*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>(生命上限|攻击力|防御力)的.*?持续伤害',
            "相邻目标伤害": r'相邻目标造成等同于.*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>(生命上限|攻击力|防御力)的.*?伤害',
            "超击破伤害": r'(造成|转化).*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>的超击破伤害',
            "击破伤害": r'(造成|转化).*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>.*?的击破伤害',
            "附加伤害": r'<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>(生命上限|攻击力|防御力)的.*?附加伤害',
            "随机伤害": r'随机单体造成.*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>(生命上限|攻击力|防御力)的.*?伤害',
            "额外伤害": r'额外造成.*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>(生命上限|攻击力|防御力)的.*?伤害',
            "技能伤害": r'造成(最多)?等同于.*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>(生命上限|攻击力|防御力)的.*?伤害',
            "伤害上限": r'最多不超过.*?(生命上限|攻击力|防御力)的<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>',
            lambda m: f"{m.group(1)}降低": r'敌方.*?(攻击力|防御力|抗性)降低<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>',
            "暴伤提高百分比": r'暴击伤害提高，提高数值等同于.*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>暴击伤害(\+)?',
            "暴伤提高": r'暴击伤害\+<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>',
            "百分比回复": r'回复等同于.*?<nobr>\$'+str(key)+r'\[(i|f1)\]%</nobr>(攻击力|生命上限|防御力)(的生命值|\+)',
            "固定值回复": r'(回复|\+)<nobr>\$'+str(key)+r'\[(i|f1)\]</nobr>(点的|的)生命值',
            f"参数{key}": r'<nobr>\$'+str(key)+r'\[(i|f1)\](%)?</nobr>'
        }

        for name_or_func, pattern in patterns.items():
            match = re.search(pattern, desc)
            if match:
                potential_name = name_or_func(match) if callable(name_or_func) else name_or_func

                if potential_name.startswith("参数"):
                    return potential_name

                if potential_name in used_names:
                    continue
                return potential_name
        return f"参数{key}"

    def process_and_format_skill(skill_data, desc_text):
        tables = {}
        skill_levels = skill_data.get("Level", {})
        if skill_levels:
            param_values_by_index = {}
            first_level_key = next((k for k in sorted(skill_levels.keys(), key=int) if skill_levels[k].get("ParamList")), None)
            if first_level_key:
                num_params = len(skill_levels[first_level_key].get("ParamList", []))
                sorted_levels = sorted(skill_levels.keys(), key=int)
                for i in range(num_params): param_values_by_index[i] = []
                for level_str in sorted_levels:
                    current_level_params = skill_levels[level_str].get("ParamList", [])
                    for i in range(num_params): param_values_by_index[i].append(current_level_params[i] if i < len(current_level_params) else 0)
                for i in range(num_params):
                    formatted_values = [format_number(v) for v in param_values_by_index.get(i, [])]
                    key = str(i + 1)
                    tables[key] = {"name": "参数", "isSame": len(set(formatted_values)) <= 1, "values": formatted_values}
    
        desc = clean_and_format_description(desc_text)
        final_desc, final_tables = format_skill_description_and_tables(desc, tables)
        used_smart_names = set()
        for k, v in final_tables.items():
            name = smart_table_name(final_desc, k, v, used_smart_names)
            v["name"] = name
            if not name.startswith("参数"):
                used_smart_names.add(name)

        spbase = skill_data.get("SPBase")
        if spbase is not None and spbase != "" and spbase > 0:
            sp_key = str(max([int(k) for k in final_tables.keys()] or [0]) + 1)
            final_tables[sp_key] = {"name": "能量恢复", "isSame": True, "values": [format_number(spbase)]}

        keys_to_remove = []
        for k, v in final_tables.items():
            is_static = v.get("isSame", False)
            is_energy_recovery = v.get("name") == "能量恢复"
            if is_static and not is_energy_recovery:
                keys_to_remove.append(k)

        for k in keys_to_remove:
            if k in final_tables:
                del final_tables[k]

        return final_desc, final_tables

    # 处理mt技能
    memosprite = source_data.get("Memosprite")
    if memosprite and isinstance(memosprite, dict):
        ms_skills = memosprite.get("Skills", {})
        mt_skill_list = sorted(
            [(ms_id, ms_skill) for ms_id, ms_skill in ms_skills.items() if ms_skill.get("Type") is None and ms_skill.get("Desc")],
            key=lambda item: int(item[0])
        )

        if len(mt_skill_list) >= 1:
            ms_id, ms_skill = mt_skill_list[0]
            final_desc, final_tables = process_and_format_skill(ms_skill, ms_skill.get("Desc", ""))
            target_data["talent"]["mt"] = {
                "id": int(ms_id), "name": ms_skill.get("Name", "忆灵天赋") or "忆灵天赋", "type": "忆灵天赋",
                "tag": map_skill_tag(ms_skill.get("Tag")), "desc": final_desc, "tables": final_tables
            }
            target_data["talentId"][str(ms_id)] = "mt"
        
        if len(mt_skill_list) >= 2:
            ms_id, ms_skill = mt_skill_list[1]
            final_desc, final_tables = process_and_format_skill(ms_skill, ms_skill.get("Desc", ""))
            target_data["talent"]["mt2"] = {
                "id": int(ms_id), "name": ms_skill.get("Name", "忆灵天赋") or "忆灵天赋", "type": "忆灵天赋",
                "tag": map_skill_tag(ms_skill.get("Tag")), "desc": final_desc, "tables": final_tables
            }
            target_data["talentId"][str(ms_id)] = "mt2"

        if len(mt_skill_list) >= 3:
            ms_id, ms_skill = mt_skill_list[2]
            final_desc, final_tables = process_and_format_skill(ms_skill, ms_skill.get("Desc", ""))
            target_data["talent"]["mt1"] = {
                "id": int(ms_id), "name": ms_skill.get("Name", "忆灵天赋") or "忆灵天赋", "type": "忆灵天赋",
                "tag": map_skill_tag(ms_skill.get("Tag")), "desc": final_desc, "tables": final_tables
            }
            target_data["talentId"][str(ms_id)] = "mt1"

        if len(mt_skill_list) > 3:
            extra_skills_to_merge["mt"] = []
            for ms_id, ms_skill in mt_skill_list[3:]:
                final_desc, final_tables = process_and_format_skill(ms_skill, ms_skill.get("Desc", ""))
                skill_name = ms_skill.get("Name", "忆灵天赋") or "忆灵天赋"
                extra_skills_to_merge["mt"].append((skill_name, final_desc, final_tables))

    # 处理常规技能
    skills_data = source_data.get("Skills", {})
    skill_type_to_base_key = {"Normal": "a", "BPSkill": "e", "Ultra": "q", "Passive": "t", "Maze": "z"}
    key_counters = {key: 1 for key in skill_type_to_base_key.values()}
    for source_skill_id in sorted(skills_data.keys()):
        skill_data = skills_data[source_skill_id]
        if not skill_data.get("SimpleDesc") or not skill_data.get("Desc"): continue
        skill_type = skill_data.get("Type")
        if skill_type == "MazeNormal": continue
        
        base_key = skill_type_to_base_key.get("Passive" if skill_type is None else skill_type)
        if not base_key: continue
        
        count = key_counters[base_key]
        key_counters[base_key] += 1

        final_desc, final_tables = process_and_format_skill(skill_data, skill_data.get("Desc", ""))
        skill_name = skill_data.get("Name", "未知技能") or "未知技能"

        if count <= 2:
            target_key = base_key if count == 1 else f"{base_key}2"
            original_skill_id_int = skill_data.get("Id", int(source_skill_id) if source_skill_id.isdigit() else 0)
            target_data["talent"][target_key] = {
                "id": original_skill_id_int, "name": skill_name, "type": map_skill_type(skill_type), 
                "tag": map_skill_tag(skill_data.get("Tag")), "desc": final_desc, "tables": final_tables
            }
            target_data["talentId"][str(original_skill_id_int)] = target_key
        else:
            if base_key not in extra_skills_to_merge:
                extra_skills_to_merge[base_key] = []
            extra_skills_to_merge[base_key].append((skill_name, final_desc, final_tables))

    # 处理me技能
    if memosprite and isinstance(memosprite, dict):
        ms_skills = memosprite.get("Skills", {})
        me_counter = 1
        for ms_id in sorted(ms_skills.keys()):
            ms_skill = ms_skills[ms_id]
            ms_type = ms_skill.get("Type")
            if ms_type is None or ms_type != "Servant" or not ms_skill.get("Desc"): continue

            base_key, skill_type_name = "me", "忆灵技"
            count = me_counter
            me_counter += 1
            
            final_desc, final_tables = process_and_format_skill(ms_skill, ms_skill.get("Desc", ""))
            skill_name = ms_skill.get("Name", skill_type_name) or skill_type_name
            
            if count <= 2:
                target_key = base_key if count == 1 else f"{base_key}2"
                target_data["talent"][target_key] = {
                    "id": int(ms_id) if ms_id.isdigit() else ms_id, "name": skill_name, "type": skill_type_name,
                    "tag": map_skill_tag(ms_skill.get("Tag")), "desc": final_desc, "tables": final_tables
                }
                target_data["talentId"][str(ms_id)] = target_key
            else:
                if base_key not in extra_skills_to_merge:
                    extra_skills_to_merge[base_key] = []
                extra_skills_to_merge[base_key].append((skill_name, final_desc, final_tables))

    # 处理专属技能
    unique_data = source_data.get("Unique")
    if unique_data and isinstance(unique_data, dict):
        base_key = "t" 
        for unique_id, unique in unique_data.items():
            if not unique.get("Name") or not unique.get("Desc"):
                continue
                
            count = key_counters[base_key]
            key_counters[base_key] += 1

            skill_name = unique.get("Name")
            final_desc = format_cons_description(unique.get("Desc", ""), unique.get("ParamList", []))
            final_tables = {} 

            if count <= 2:
                target_key = f"{base_key}2" if count == 2 else base_key
                skill_id = f"{character_id}9{count}"

                target_data["talent"][target_key] = {
                    "id": int(skill_id), "name": skill_name, "type": "天赋[专属]",
                    "tag": unique.get("Tag", ""), "desc": final_desc, "tables": final_tables
                }
                target_data["talentId"][str(skill_id)] = target_key
            else:
                if base_key not in extra_skills_to_merge:
                    extra_skills_to_merge[base_key] = []
                extra_skills_to_merge[base_key].append((skill_name, final_desc, final_tables))


    # 通用合并逻辑
    for base_key, skills_to_merge in extra_skills_to_merge.items():
        merge_target_key = f"{base_key}2"
        if not skills_to_merge or merge_target_key not in target_data["talent"]:
            continue

        target_skill = target_data["talent"][merge_target_key]
        orig_desc = target_skill.get("desc", "")
        orig_tables = target_skill.get("tables", {})

        desc_parts = [orig_desc] if orig_desc else []
        merged_tables = dict(orig_tables)
        table_key_counter = max([int(k) for k in merged_tables.keys()] or [0])

        for name, desc, tables in skills_to_merge:
            key_map = {}
            for old_k in sorted(tables.keys(), key=int):
                table_key_counter += 1
                new_k = str(table_key_counter)
                key_map[old_k] = new_k
                v = dict(tables[old_k])
                original_table_name = v.get("name", f"参数{new_k}")

                parts = re.split('[·•]', name)
                prefix_name = parts[-1]

                v["name"] = f"{prefix_name}•{original_table_name}"

                merged_tables[new_k] = v

            def repl(m):
                return f"${key_map.get(m.group(1), m.group(1))}{m.group(2)}"

            desc_fixed = re.sub(r'\$(\d+)(\[.*?\]%?)', repl, desc)
            desc_parts.append(f"{name}:<br />{desc_fixed}")

        target_skill["desc"] = "<br /><br />".join(desc_parts)
        target_skill["tables"] = merged_tables

    # 按指定顺序排序天赋
    unsorted_talents = target_data['talent']
    full_order = [
        'a', 'e', 'q', 't', 'z',
        'a2', 'e2', 'q2', 't2', 'z2',
        'me', 'me2',
        'mt', 'mt1', 'mt2'
    ]

    sorted_talents = {}
    for key in full_order:
        if key in unsorted_talents:
            sorted_talents[key] = unsorted_talents[key]

    for key, value in unsorted_talents.items():
        if key not in sorted_talents:
            sorted_talents[key] = value

    target_data['talent'] = sorted_talents

    ranks_data = source_data.get("Ranks", {})
    target_data["cons"] = {}
    for i in range(1, 7):
        rank_key = str(i)
        rank_data = ranks_data.get(rank_key)
        if rank_data:
            target_data["cons"][rank_key] = {
                "name": rank_data.get("Name", f"未知星魂 {rank_key}") or f"未知星魂 {rank_key}",
                "desc": format_cons_description(rank_data.get("Desc"), rank_data.get("ParamList")) or "暂无描述"
            }

    def extract_talent_cons(cons_dict):
        keyword_map = {"普攻": "a", "战技": "e", "终结技": "q", "天赋": "t", "忆灵技": "me", "忆灵天赋": "mt"}
        cons_map = {}
        cons3_desc = cons_dict.get("3", {}).get("desc", "")
        cons5_desc = cons_dict.get("5", {}).get("desc", "")
        cons3_keys = [key for cn, key in keyword_map.items() if cn in cons3_desc]
        cons5_keys = [key for cn, key in keyword_map.items() if cn in cons5_desc]
        priority = ["e", "q", "a", "t", "me", "mt"]
        
        for keys, idx in [(cons3_keys, "3"), (cons5_keys, "5")]:
            for p in priority:
                if p in keys:
                    cons_map[idx] = p
                    break
        for key in cons3_keys: cons_map[key] = 3
        for key in cons5_keys: cons_map[key] = 5
        return cons_map

    target_data["talentCons"] = extract_talent_cons(target_data["cons"])

    target_data["attr"] = {}
    promo_max_level = {0: 20, 1: 30, 2: 40, 3: 50, 4: 60, 5: 70, 6: 80}
    for promo_key, stats_data in stats_data_all.items():
        if promo_key.isdigit():
            promo_level = int(promo_key)
            cost = {get_mapped_material_id(item.get("ItemID")): item.get("ItemNum", 0) for item in stats_data.get("Cost", []) if item.get("ItemID")}
            target_data["attr"][promo_key] = {
                "promote": promo_level, "maxLevel": promo_max_level.get(promo_level, 20 + promo_level * 10),
                "cost": cost,
                "grow": {"atk": stats_data.get("AttackAdd", 0.0), "hp": stats_data.get("HPAdd", 0.0), "def": stats_data.get("DefenceAdd", 0.0), "speed": 0},
                "attrs": {"atk": stats_data.get("AttackBase", 0.0), "hp": stats_data.get("HPBase", 0.0), "def": stats_data.get("DefenceBase", 0.0), "speed": stats_data.get("SpeedBase", 0), "cpct": round(stats_data.get("CriticalChance", 0.0) * 100, 2), "cdmg": round(stats_data.get("CriticalDamage", 0.0) * 100, 2), "aggro": stats_data.get("BaseAggro", 0)}
            }

    skill_trees_data = source_data.get("SkillTrees", {})
    target_data["tree"] = {}
    target_data["treeData"] = {}

    relevant_tree_nodes = {}
    for point_key, point_levels in skill_trees_data.items():
        level_1_data = point_levels.get("1")
        if level_1_data and level_1_data.get("PointType") in [1, 3]:
            point_id = level_1_data.get("PointID")
            if point_id: relevant_tree_nodes[str(point_id)] = level_1_data

    for point_id_str, node_data in relevant_tree_nodes.items():
        point_type = node_data.get("PointType")
        cost_list = node_data.get("MaterialList", [])
        processed_cost = {get_mapped_material_id(item.get("ItemID")): item.get("ItemNum", 0) for item in cost_list if item.get("ItemID")}

        final_desc, final_data = "", {}
        if point_type == 1:
            if status_adds := node_data.get("StatusAddList"):
                prop_type, prop_value = status_adds[0].get("PropertyType"), status_adds[0].get("Value")
                prop_name = node_data.get("PointName", "")
                if elem_key := map_tree_buff_key_elem(prop_type, prop_name):
                    if prop_value is not None: final_data[elem_key] = round(prop_value * 100, 2)
                elif (mapped_key := map_tree_buff_key(prop_type)) and (scaled_value := scale_tree_buff_value(prop_type, prop_value)) is not None:
                    final_data[mapped_key] = scaled_value
        elif point_type == 3:
            final_desc = format_cons_description(node_data.get("PointDesc"), node_data.get("ParamList")) or "暂无描述"

        tree_node = {
            "id": node_data.get("PointID") or 0, "type": "buff" if point_type == 1 else "skill",
            "name": node_data.get("PointName", "未知行迹") or "未知行迹",
            "levelReq": node_data.get("AvatarLevelLimit", 0) or 0
        }
        if final_desc: tree_node["desc"] = final_desc
        tree_node["cost"] = processed_cost
        if final_data: tree_node["data"] = final_data
        tree_node["children"] = []
        target_data["treeData"][point_id_str] = tree_node

    all_relevant_ids = set(relevant_tree_nodes.keys())
    nodes_with_prepoints = set()
    for point_id_str, node_data in relevant_tree_nodes.items():
        for pre_point_id in node_data.get("PrePoint", []) or []:
            pre_point_id_str = str(pre_point_id)
            if pre_point_id_str in all_relevant_ids:
                nodes_with_prepoints.add(point_id_str)
                if pre_point_id_str in target_data["treeData"]:
                    target_data["treeData"][pre_point_id_str]["children"].append(point_id_str)
    
    root_ids = all_relevant_ids - nodes_with_prepoints

    tree_idx_counter = 1
    for point_id_str, node in list(target_data["treeData"].items()):
        is_root = point_id_str in root_ids
        
        items = list(node.items())
        try:
            type_index = [i for i, item in enumerate(items) if item[0] == 'type'][0]
            items.insert(type_index + 1, ('root', is_root))
            node = dict(items)
        except IndexError:
            node['root'] = is_root

        if node.get("root") and node.get("type") == "skill" and node.get("desc"):
            items = list(node.items())
            try:
                cost_index = [i for i, item in enumerate(items) if item[0] == 'cost'][0]
                items.insert(cost_index + 1, ('idx', tree_idx_counter))
                node = dict(items)
                tree_idx_counter += 1
            except IndexError:
                node['idx'] = tree_idx_counter
                tree_idx_counter += 1
        
        target_data["treeData"][point_id_str] = node

    for node in target_data["treeData"].values():
        if not node.get("children"):
            node.pop("children", None)

    for point_id_str, node_data in target_data["treeData"].items():
        if node_data.get("type") == "buff" and (data := node_data.get("data")):
            key, value = next(iter(data.items()))
            tree_key_map = {"atkPct": "atk", "hpPct": "hp", "defPct": "def", "auantum": "quantum"}
            tree_key = tree_key_map.get(key, key)
            tree_value = round(value * 0.01, 5) if key == "speed" else value
            target_data["tree"][point_id_str] = {"key": tree_key, "value": tree_value}
            
    return recursively_format_numbers(target_data)

def load_and_prepare_material_data(filepath):
    """加载并预处理本地材料映射文件。"""
    global material_name_to_id_map, material_mapping_data
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            material_mapping_data = json.load(f)
        for category in material_mapping_data.values():
            if isinstance(category, dict):
                for item_id, item_data in category.items():
                    if isinstance(item_data, dict) and 'name' in item_data and 'id' in item_data:
                        material_name_to_id_map[item_data['name']] = item_data['id']
        print(f"已成功从 {filepath} 预加载 {len(material_name_to_id_map)} 条材料映射。")
        return True
    except FileNotFoundError:
        print(f"警告：未找到材料映射文件 {filepath}。如果遇到新材料，将尝试创建此文件。")
        material_mapping_data = {}
        return True
    except json.JSONDecodeError:
        print(f"错误：无法解析 {filepath} 的JSON内容。")
        return False
    except Exception as e:
        print(f"错误：加载 {filepath} 时发生未知错误：{e}")
        return False

def download_and_save_image(url, save_path):
    try:
        resp = requests.get(url, timeout=10, stream=True)
        if resp.status_code == 200:
            with open(save_path, 'wb') as f:
                for chunk in resp.iter_content(1024):
                    f.write(chunk)
            return True
        return False
    except Exception as e:
        return False

def ensure_dir_exists(path):
    os.makedirs(path, exist_ok=True)

def get_character_dir():
    """返回角色数据绝对路径"""
    return CHARACTER_DIR_ABS_PATH

def download_material_image(item_id, item_name, category, base_dir):
    if not all([item_id, item_name, category, base_dir]):
        return

    url = f"https://api.hakush.in/hsr/UI/itemfigures/{item_id}.webp"
    dest_dir = os.path.join(base_dir, category)
    save_path = os.path.join(dest_dir, f"{item_name}.webp")

    if os.path.exists(save_path):
        return

    ensure_dir_exists(dest_dir)
    print(f"    - 正在下载新材料图片: {item_name}.webp ...", end=" ")
    if download_and_save_image(url, save_path):
        print("成功")
    else:
        print("失败")


def download_character_images(char_id, char_key, talents, cons, tree_data, output_dir):
    img_dir = os.path.join(output_dir, "imgs")
    ensure_dir_exists(img_dir)
    char_id_str = str(char_id)
    
    # 只下载这些技能图标
    main_talent_keys = ['a', 'e', 'q', 't', 'z', 'me', 'mt']
    skill_key_map = {
        "a": "Normal", "e": "BP", "q": "Ultra", "t": "Passive", "z": "Maze",
        "me": "Servant", "mt": "ServantPassive"
    }

    for key in main_talent_keys:
        if key not in talents:
            continue

        skill_type = skill_key_map.get(key)
        if not skill_type:
            continue

        save_path = os.path.join(img_dir, f"talent-{key}.webp")
        
        # 终结技图标有特殊情况，可能为 on 状态
        if skill_type == "Ultra":
            url1 = f"https://homdgcat.wiki/images/skillicons/avatar/{char_id_str}/SkillIcon_{char_id_str}_Ultra.png"
            if not download_and_save_image(url1, save_path):
                url2 = f"https://homdgcat.wiki/images/skillicons/avatar/{char_id_str}/SkillIcon_{char_id_str}_Ultra_on.png"
                download_and_save_image(url2, save_path)
        else:
            url = f"https://homdgcat.wiki/images/skillicons/avatar/{char_id_str}/SkillIcon_{char_id_str}_{skill_type}.png"
            download_and_save_image(url, save_path)

    # 下载星魂图标
    for cons_key in cons.keys():
        url = f"https://homdgcat.wiki/images/skillicons/avatar/{char_id_str}/SkillIcon_{char_id_str}_Rank{cons_key}.png"
        download_and_save_image(url, os.path.join(img_dir, f"cons-{cons_key}.webp"))

    # 下载行迹图标
    for node in tree_data.values():
        if node.get("type") == "skill" and "idx" in node:
            url = f"https://homdgcat.wiki/images/skillicons/avatar/{char_id_str}/SkillIcon_{char_id_str}_SkillTree{node['idx']}.png"
            download_and_save_image(url, os.path.join(img_dir, f"tree-{node['idx']}.webp"))

    # 下载角色相关图像
    download_and_save_image(f"https://homdgcat.wiki/images/avatardrawcard/{char_id_str}.png", os.path.join(img_dir, "splash.webp"))
    download_and_save_image(f"https://homdgcat.wiki/images/avatarshopicon/avatar/{char_id_str}.png", os.path.join(img_dir, "preview.webp"))
    download_and_save_image(f"https://homdgcat.wiki/images/avataricon/avatar/{char_id_str}.png", os.path.join(img_dir, "face.webp"))

if __name__ == "__main__":
    if not os.path.exists(MIAO_PLUGIN_PATH):
        print(f"错误：请正确配置miao-plugin路径: {MIAO_PLUGIN_PATH}")
        exit(1)

    if not load_and_prepare_material_data(MAPPING_FILE_PATH):
        exit(1)

    character_ids = []
    if USE_API_CHARACTER_LIST:
        try:
            resp = requests.get(API_CHARACTER_LIST_URL, timeout=10)
            resp.raise_for_status()
            api_ids = resp.json().get("character", [])
            character_ids.extend(api_ids if isinstance(api_ids, list) else [])
        except Exception as e:
            print(f"错误：拉取角色ID列表失败：{e}")
    
    character_ids.extend(cid for cid in CUSTOM_CHARACTER_IDS if cid not in character_ids)
    character_ids = sorted(list({int(i) for i in character_ids if str(i).isdigit()}))

    if not character_ids:
        print("错误：未获取到任何角色ID。")
        exit(1)

    character_dir = get_character_dir()
    ensure_dir_exists(character_dir)
    summary_path = os.path.join(character_dir, "data.json")
    summary = {}
    if os.path.exists(summary_path):
        try:
            with open(summary_path, 'r', encoding='utf-8') as f:
                summary = json.load(f)
        except Exception: pass
    
    new_data_added = False
    converted_list = []
    
    for cid in character_ids:
        try:
            url = API_CHARACTER_DATA_URL.format(cid)
            resp = requests.get(url, timeout=10)
            resp.raise_for_status()
            source_data = resp.json()

            print(f"\n正在转换角色 {cid} ({source_data.get('Name', '未知')})...")
            converted_data = convert_character_data(source_data, f"{cid}.json")
            
            char_name = converted_data.get("name")
            if not char_name or char_name == '？？？':
                print(f"警告：角色 {cid} 名称获取失败，跳过该角色。")
                continue
            
            converted_list.append(converted_data)
            char_dir = os.path.join(character_dir, char_name)
            ensure_dir_exists(char_dir)
            
            output_file_path = os.path.join(char_dir, "data.json")
            with open(output_file_path, 'w', encoding='utf-8') as f:
                json.dump(converted_data, f, indent=2, ensure_ascii=False)
            print(f"已输出到 {output_file_path}")

            download_character_images(
                converted_data.get("id"), converted_data.get("key"),
                converted_data.get("talent", {}), converted_data.get("cons", {}),
                converted_data.get("treeData", {}), char_dir
            )
            time.sleep(0.1)
        except Exception as e:
            print(f"角色 {cid} 处理失败：{e}")
            import traceback
            traceback.print_exc()

    for data in converted_list:
        sid = str(data.get("id"))
        if sid not in summary:
            new_data_added = True
        # 更新/覆盖
        summary[sid] = {
            "id": data.get("id"), "key": data.get("key"), "name": data.get("name"),
            "star": data.get("star"), "elem": data.get("elem"), "weapon": data.get("weapon"),
            "sp": data.get("sp"), "talentId": data.get("talentId"), "talentCons": data.get("talentCons"),
        }
    
    summary_sorted = {k: summary[k] for k in sorted(summary.keys(), key=int)}
    with open(summary_path, 'w', encoding='utf-8') as f:
        json.dump(summary_sorted, f, indent=2, ensure_ascii=False)
    print(f"\n角色摘要已更新并写入 {summary_path}")
    # 使用数据源id增量写入材料(理论上保持唯一id即可)
    if AUTO_ADD_MISSING_MATERIALS and newly_added_material_ids:
        print(f"\n发现了 {len(newly_added_material_ids)} 个新材料，正在更新材料映射文件...")
        try:
            with open(MAPPING_FILE_PATH, 'w', encoding='utf-8') as f:
                json.dump(material_mapping_data, f, indent=2, ensure_ascii=False)
            print(f"成功！材料映射文件 '{MAPPING_FILE_PATH}' 已更新。")
        except Exception as e:
            print(f"错误：写入材料映射文件失败：{e}")